Organizing Command 您所在的位置:网站首页 what is a class intake Organizing Command

Organizing Command

2023-08-08 22:59| 来源: 网络整理| 查看: 265

Defining Commands

In larger robot codebases, multiple copies of the same command need to be used in many different places. For instance, a command that runs a robot’s intake might be used in teleop, bound to a certain button; as part of a complicated command group for an autonomous routine; and as part of a self-test sequence.

As an example, let’s look at some ways to define a simple command that simply runs the robot’s intake forward at full power until canceled.

Inline Commands

The easiest and most expressive way to do this is with a StartEndCommand:

Command runIntake = Commands.startEnd(() -> intake.set(1), () -> intake.set(0), intake); frc2::CommandPtr runIntake = frc2::cmd::StartEnd([&intake] { intake.Set(1.0); }, [&intake] { intake.Set(0); }, {&intake});

This is sufficient for commands that are only used once. However, for a command like this that might get used in many different autonomous routines and button bindings, inline commands everywhere means a lot of repetitive code:

// RobotContainer.java intakeButton.whileTrue(Commands.startEnd(() -> intake.set(1.0), () -> intake.set(0), intake)); Command intakeAndShoot = Commands.startEnd(() -> intake.set(1.0), () -> intake.set(0), intake) .alongWith(new RunShooter(shooter)); Command autonomousCommand = Commands.sequence( Commands.startEnd(() -> intake.set(1.0), () -> intake.set(0.0), intake).withTimeout(5.0), Commands.waitSeconds(3.0), Commands.startEnd(() -> intake.set(1.0), () -> intake.set(0.0), intake).withTimeout(5.0) ); intakeButton.WhileTrue(frc2::cmd::StartEnd([&intake] { intake.Set(1.0); }, [&intake] { intake.Set(0); }, {&intake})); frc2::CommandPtr intakeAndShoot = frc2::cmd::StartEnd([&intake] { intake.Set(1.0); }, [&intake] { intake.Set(0); }, {&intake}) .AlongWith(RunShooter(&shooter).ToPtr()); frc2::CommandPtr autonomousCommand = frc2::cmd::Sequence( frc2::cmd::StartEnd([&intake] { intake.Set(1.0); }, [&intake] { intake.Set(0); }, {&intake}).WithTimeout(5.0_s), frc2::cmd::Wait(3.0_s), frc2::cmd::StartEnd([&intake] { intake.Set(1.0); }, [&intake] { intake.Set(0); }, {&intake}).WithTimeout(5.0_s) );

Creating one StartEndCommand instance and putting it in a variable won’t work here, since once an instance of a command is added to a command group it is effectively “owned” by that command group and cannot be used in any other context.

Instance Command Factory Methods

One way to solve this quandary is using the “factory method” design pattern: a function that returns a new object every invocation, according to some specification. Using command composition, a factory method can construct a complex command object with merely a few lines of code.

For example, a command like the intake-running command is conceptually related to exactly one subsystem: the Intake. As such, it makes sense to put a runIntakeCommand method as an instance method of the Intake class:

Note

In this document we will name factory methods as lowerCamelCaseCommand, but teams may decide on other conventions. In general, it is recommended to end the method name with Command if it might otherwise be confused with an ordinary method (e.g. intake.run might be the name of a method that simply turns on the intake).

public class Intake extends SubsystemBase { // [code for motor controllers, configuration, etc.] // ... public Command runIntakeCommand() { // implicitly requires `this` return this.startEnd(() -> this.set(1.0), () -> this.set(0.0)); } } frc2::CommandPtr Intake::RunIntakeCommand() { // implicitly requires `this` return this->StartEnd([this] { this->Set(1.0); }, [this] { this->Set(0); }); }

Notice how since we are in the Intake class, we no longer refer to intake; instead, we use the this keyword to refer to the current instance.

Since we are inside the Intake class, technically we can access private variables and methods directly from within the runIntakeCommand method, thus not needing intermediary methods. (For example, the runIntakeCommand method can directly interface with the motor controller objects instead of calling set().) On the other hand, these intermediary methods can reduce code duplication and increase encapsulation. Like many other choices outlined in this document, this tradeoff is a matter of personal preference on a case-by-case basis.

Using this new factory method in command groups and button bindings is highly expressive:

intakeButton.whileTrue(intake.runIntakeCommand()); Command intakeAndShoot = intake.runIntakeCommand().alongWith(new RunShooter(shooter)); Command autonomousCommand = Commands.sequence( intake.runIntakeCommand().withTimeout(5.0), Commands.waitSeconds(3.0), intake.runIntakeCommand().withTimeout(5.0) ); intakeButton.WhileTrue(intake.RunIntakeCommand()); frc2::CommandPtr intakeAndShoot = intake.RunIntakeCommand().AlongWith(RunShooter(&shooter).ToPtr()); frc2::CommandPtr autonomousCommand = frc2::cmd::Sequence( intake.RunIntakeCommand().WithTimeout(5.0_s), frc2::cmd::Wait(3.0_s), intake.RunIntakeCommand().WithTimeout(5.0_s) );

Adding a parameter to the runIntakeCommand method to provide the exact percentage to run the intake is easy and allows for even more flexibility.

public Command runIntakeCommand(double percent) { return new StartEndCommand(() -> this.set(percent), () -> this.set(0.0), this); } frc2::CommandPtr Intake::RunIntakeCommand() { // implicitly requires `this` return this->StartEnd([this, percent] { this->Set(percent); }, [this] { this->Set(0); }); }

For instance, this code creates a command group that runs the intake forwards for two seconds, waits for two seconds, and then runs the intake backwards for five seconds.

Command intakeRunSequence = intake.runIntakeCommand(1.0).withTimeout(2.0) .andThen(Commands.waitSeconds(2.0)) .andThen(intake.runIntakeCommand(-1.0).withTimeout(5.0)); frc2::CommandPtr intakeRunSequence = intake.RunIntakeCommand(1.0).WithTimeout(2.0_s) .AndThen(frc2::cmd::Wait(2.0_s)) .AndThen(intake.RunIntakeCommand(-1.0).WithTimeout(5.0_s));

This approach is recommended for commands that are conceptually related to only a single subsystem, and is very concise. However, it doesn’t fare well with commands related to more than one subsystem: passing in other subsystem objects is unintuitive and can cause race conditions and circular dependencies, and thus should be avoided. Therefore, this approach is best suited for single-subsystem commands, and should be used only for those cases.

Static Command Factories

Instance factory methods work great for single-subsystem commands. However, complicated robot actions (like the ones often required during the autonomous period) typically need to coordinate multiple subsystems at once. When we want to define an inline command that uses multiple subsystems, it doesn’t make sense for the command factory to live in any single one of those subsystems. Instead, it can be cleaner to define the command factory methods statically in some external class:

Note

The sequence and parallel static factories construct sequential and parallel command groups: this is equivalent to the andThen and alongWith decorators, but can be more readable. Their use is a matter of personal preference.

public class AutoRoutines { public static Command driveAndIntake(Drivetrain drivetrain, Intake intake) { return Commands.sequence( Commands.parallel( drivetrain.driveCommand(0.5, 0.5), intake.runIntakeCommand(1.0) ).withTimeout(5.0), Commands.parallel( drivetrain.stopCommand(); intake.stopCommand(); ) ); } } // TODO Non-Static Command Factories

If we want to avoid the verbosity of adding required subsystems as parameters to our factory methods, we can instead construct an instance of our AutoRoutines class and inject our subsystems through the constructor:

public class AutoRoutines { private Drivetrain drivetrain; private Intake intake; public AutoRoutines(Drivetrain drivetrain, Intake intake) { this.drivetrain = drivetrain; this.intake = intake; } public Command driveAndIntake() { return Commands.sequence( Commands.parallel( drivetrain.driveCommand(0.5, 0.5), intake.runIntakeCommand(1.0) ).withTimeout(5.0), Commands.parallel( drivetrain.stopCommand(); intake.stopCommand(); ) ); } public Command driveThenIntake() { return Commands.sequence( drivetrain.driveCommand(0.5, 0.5).withTimeout(5.0), drivetrain.stopCommand(), intake.runIntakeCommand(1.0).withTimeout(5.0), intake.stopCommand() ); } } // TODO

Then, elsewhere in our code, we can instantiate an single instance of this class and use it to produce several commands:

AutoRoutines autoRoutines = new AutoRoutines(this.drivetrain, this.intake); Command driveAndIntake = autoRoutines.driveAndIntake(); Command driveThenIntake = autoRoutines.driveThenIntake(); Command drivingAndIntakingSequence = Commands.sequence( autoRoutines.driveAndIntake(), autoRoutines.driveThenIntake() ); // TODO Capturing State in Inline Commands

Inline commands are extremely concise and expressive, but do not offer explicit support for commands that have their own internal state (such as a drivetrain trajectory following command, which may encapsulate an entire controller). This is often accomplished by instead writing a Command class, which will be covered later in this article.

However, it is still possible to ergonomically write a stateful command composition using inline syntax, so long as we are working within a factory method. To do so, we declare the state as a method local and “capture” it in our inline definition. For example, consider the following instance command factory to turn a drivetrain to a specific angle with a PID controller:

Note

The Subsystem.run and Subsystem.runOnce factory methods sugar the creation of a RunCommand and an InstantCommand requiring this subsystem.

public Command turnToAngle(double targetDegrees) { // Create a controller for the inline command to capture PIDController controller = new PIDController(Constants.kTurnToAngleP, 0, 0); // We can do whatever configuration we want on the created state before returning from the factory controller.setPositionTolerance(Constants.kTurnToAngleTolerance); // Try to turn at a rate proportional to the heading error until we're at the setpoint, then stop return run(() -> arcadeDrive(0,-controller.calculate(gyro.getHeading(), targetDegrees))) .until(controller::atSetpoint) .andThen(runOnce(() -> arcadeDrive(0, 0))); } // TODO

This pattern works very well in Java so long as the captured state is “effectively final” - i.e., it is never reassigned. This means that we cannot directly define and capture primitive types (e.g. int, double, boolean) - to circumvent this, we need to wrap any state primitives in a mutable container type (the same way PIDController wraps its internal kP, kI, and kD values).

Writing Command Classes

Another possible way to define reusable commands is to write a class that represents the command. This is typically done by subclassing either CommandBase or one of the CommandGroup classes.

Subclassing CommandBase

Returning to our simple intake command from earlier, we could do this by creating a new subclass of CommandBase that implements the necessary initialize and end methods.

public class RunIntakeCommand extends CommandBase { private Intake m_intake; public RunIntakeCommand(Intake intake) { this.m_intake = intake; addRequirements(intake); } @Override public void initialize() { m_intake.set(1.0); } @Override public void end(boolean interrupted) { m_intake.set(0.0); } // execute() defaults to do nothing // isFinished() defaults to return false } // TODO

This, however, is just as cumbersome as the original repetitive code, if not more verbose. The only two lines that really matter in this entire file are the two calls to intake.set(), yet there are over 20 lines of boilerplate code! Not to mention, doing this for a lot of robot actions quickly clutters up a robot project with dozens of small files. Nevertheless, this might feel more “natural,” particularly for programmers who prefer to stick closely to an object-oriented model.

This approach should be used for commands with internal state (not subsystem state!), as the class can have fields to manage said state. It may also be more intuitive to write commands with complex logic as classes, especially for those less experienced with command composition. As the command is detached from any specific subsystem class and the required subsystem objects are injected through the constructor, this approach deals well with commands involving multiple subsystems.

Subclassing Command Groups

If we wish to write composite commands as their own classes, we may write a constructor-only subclass of the most exterior group type. For example, an intake-then-outtake sequence (with single-subsystem commands defined as instance factory methods) can look like this:

public class IntakeThenOuttake extends SequentialCommandGroup { public IntakeThenOuttake(Intake intake) { super( intake.runIntakeCommand(1.0).withTimeout(2.0), new WaitCommand(2.0), intake.runIntakeCommand(-1).withTimeout(5.0) ); } } // TODO

This is relatively short and minimizes boilerplate. It is also comfortable to use in a purely object-oriented paradigm and may be more acceptable to novice programmers. However, it has some downsides. For one, it is not immediately clear exactly what type of command group this is from the constructor definition: it is better to define this in a more inline and expressive way, particularly when nested command groups start showing up. Additionally, it requires a new file for every single command group, even when the groups are conceptually related.

As with factory methods, state can be defined and captured within the command group subclass constructor, if necessary.

Summary

Approach

Primary Use Case

Single-subsystem Commands

Multi-subsystem Commands

Stateful Commands

Complex Logic Commands

Instance Factory Methods

Single-subsystem commands

Excels at them

No

Yes, but must obey capture rules

Yes

Subclassing CommandBase

Stateful commands

Very verbose

Relatively verbose

Excels at them

Yes; may be more natural than other approaches

Static and Instance Command Factories

Multi-subsystem commands

Yes

Yes

Yes, but must obey capture rules

Yes

Subclassing Command Groups

Multi-subsystem command groups

Yes

Yes

Yes, but must obey capture rules

Yes



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有